﻿namespace Jsl.FxCop
{
    using System.Collections.Generic;
    using System.Diagnostics.Contracts;
    using System.Linq;
    using Microsoft.FxCop.Sdk;

    /// <summary>
    /// Searches for Dispose methods that don't dispose of the components field.
    /// </summary>
    public class CallComponentsDispose : BaseIntrospectionRule
    {
        #region Constructors
        /// <summary>
        /// Initializes a new instance of the <see cref="CallComponentsDispose"/> class.
        /// </summary>
        public CallComponentsDispose()
            : base(typeof(CallComponentsDispose).Name, "Jsl.FxCop.Rules", typeof(CallComponentsDispose).Assembly)
        {
        }
        #endregion Constructors

        #region Public Methods
        /// <summary>
        /// Checks the specified type.
        /// </summary>
        /// <param name="type">The type to check.</param>
        /// <returns>The problems found if any.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "The parameter is never null.")]
        public override ProblemCollection Check(TypeNode type)
        {
            // At this level, we only care about cases where there is no Dispose override. The other Check will take care
            // of checking the Dispose method.
            var disposeMethod = type.GetMethod(Identifier.For(WellKnownMethodNames.Dispose), FrameworkTypes.Boolean);
            if (disposeMethod == null)
            {
                // If this class has a components field and it is initialized then we have a problem.
                if (CallComponentsDispose.HasComponentsField(type)
                    && CallComponentsDispose.IsComponentsCreated(type))
                {
                    this.Problems.Add(new Problem(this.GetNamedResolution("MissingDisposeMethod"), type.SourceContext));
                }
            }

            return this.Problems;
        }

        /// <summary>
        /// Checks the specified member.
        /// </summary>
        /// <param name="member">The member to check.</param>
        /// <returns>The problems found if any.</returns>
        public override ProblemCollection Check(Member member)
        {
            member.DoIfVirtualDisposeMethod(
                method =>
                {
                    if (CallComponentsDispose.HasComponentsField(method.DeclaringType))
                    {
                        this.CheckComponentsFieldIsDisposed(method);
                    }
                });

            return this.Problems;
        }
        #endregion Public Methods

        #region Private Methods
        private static IEnumerable<Statement> GetDisposeComponentCalls(StatementFlow flow)
        {
            Contract.Ensures(Contract.Result<IEnumerable<Statement>>() != null);

            return flow.Statements
                .Where(CallComponentsDispose.IsComponentsDispose);
        }

        private static bool HasComponentsField(TypeNode type)
        {
            // See if there is a components field declared in the same class as the method and is the correct interface or type.
            var componentsField = type.GetField(Identifier.For("components"));
            return (componentsField != null)
                && ((componentsField.Type.FullName == WellKnownTypeNames.Container)
                    || (componentsField.Type.FullName == WellKnownTypeNames.IContainer))
                && object.ReferenceEquals(componentsField.DeclaringType, type);
        }

        private static bool IsComponentsBinding(MemberBinding binding)
        {
            if (binding != null)
            {
                if (binding.TargetObject is This)
                {
                    var field = binding.BoundMember as Field;
                    if (field != null)
                    {
                        if (field.Name.Name == "components")
                        {
                            return true;
                        }
                    }
                }
            }

            return false;
        }

        private static bool IsComponentsCreated(TypeNode type)
        {
            // Assume the components field is not created.
            var componentsFieldIsInitialized = false;

            // The InitializeComponent method should create the value. If the method doesn't exist then exit.
            type.DoIfInitializeComponent(
                initializeComponentMethod =>
                {
                    // Search for the initialization of the components field in the InitializeComponent method.
                    new CciVisitor()
                        .AddVisitor<AssignmentStatement>(assignment =>
                            {
                                // If this is an assignment to this.components then flag the field was initialized.
                                if (CallComponentsDispose.IsComponentsBinding(assignment.Target as MemberBinding))
                                {
                                    componentsFieldIsInitialized = true;
                                }
                            })
                        .Visit(initializeComponentMethod);
                });

            return componentsFieldIsInitialized;
        }

        private static bool IsComponentsDispose(Statement statement)
        {
            var expressionStatement = statement as ExpressionStatement;
            if (expressionStatement != null)
            {
                var call = expressionStatement.Expression as MethodCall;
                if (call != null)
                {
                    var calledMethod = call.GetMethod();
                    if (calledMethod != null)
                    {
                        if ((calledMethod.FullName == WellKnownTypeNames.IDisposableDispose)
                            || (calledMethod.FullName == WellKnownTypeNames.ContainerDispose))
                        {
                            return CallComponentsDispose.IsComponentsBinding(call.GetTargetObject() as MemberBinding);
                        }
                    }
                }
            }

            return false;
        }

        private static bool IsSettingComponentsToNull(AssignmentStatement statement)
        {
            return (statement != null)
                && CallComponentsDispose.IsComponentsBinding(statement.Target as MemberBinding)
                && (statement.Source.NodeType == NodeType.Literal)
                && ((Literal)statement.Source).Value == null;
        }

        private void AddProblem(string resolutionName, SourceContext context)
        {
            this.Problems.Add(
                new Problem(
                    this.GetNamedResolution(resolutionName),
                    context));
        }

        private void CheckComponentsFieldIsDisposed(Method method)
        {
            Contract.Requires(method != null);

            var flow = new StatementFlow(method);
            var disposeComponentsCalls = CallComponentsDispose.GetDisposeComponentCalls(flow).ToArray();

            switch (disposeComponentsCalls.Length)
            {
                case 0:
                    if (CallComponentsDispose.IsComponentsCreated(method.DeclaringType))
                    {
                        this.AddProblem("MustDisposeComponents", method.SourceContext);
                    }
                    else
                    {
                        this.CheckThatComponentsIsntAccessed(flow);
                    }

                    break;

                case 1:
                    this.CheckDisposeCall(flow, disposeComponentsCalls[0]);
                    break;

                default:
                    this.AddProblem("OnlyOneDispose", method.SourceContext);
                    break;
            }
        }

        private void CheckDisposeCall(StatementFlow flow, Statement disposeComponentsCall)
        {
            // There should only be one statement following the dispose.
            var followingStatements = flow.SignificantFollowingStatements(disposeComponentsCall);
            if (followingStatements.Count() != 1)
            {
                this.AddProblem("DisposeShouldBeOnlyStatement", disposeComponentsCall.SourceContext);
                return;
            }

            // If the following statement is 'this.components = null' then ignore that statement and continue checking.
            var followingStatement = followingStatements.First();
            if (IsSettingComponentsToNull(followingStatement as AssignmentStatement))
            {
                followingStatements = flow.SignificantFollowingStatements(followingStatement);
                if (followingStatements.Count() != 1)
                {
                    this.AddProblem("DisposeShouldBeOnlyStatement", disposeComponentsCall.SourceContext);
                    return;
                }

                followingStatement = followingStatements.First();
            }

            // See what's before the dispose call.
            var previousStatements = flow.SignificantPreviousStatements(disposeComponentsCall);
            switch (previousStatements.Count())
            {
                case 0:
                    // There should be a previous statement.
                    this.AddProblem("MustCheckComponentsNull", disposeComponentsCall.SourceContext);
                    return;

                case 1:
                    break;

                default:
                    // There shouldn't be more than one statement.
                    this.AddProblem("MustDisposeComponents", disposeComponentsCall.SourceContext);
                    return;
            }

            // The previous statement should be the components null check.
            var nullCheck = previousStatements.First() as Branch;
            if (nullCheck == null)
            {
                this.AddProblem("MustCheckComponentsNull", disposeComponentsCall.SourceContext);
                return;
            }

            // The statement after the null check should have the null check as well 
            var statementsBeforeIf = flow.SignificantPreviousStatements(followingStatement);
            if (!statementsBeforeIf.Contains(nullCheck))
            {
                this.AddProblem("DisposeShouldBeOnlyStatement", disposeComponentsCall.SourceContext);
                return;
            }

            switch (nullCheck.Condition.NodeType)
            {
                case NodeType.Local:
                    // There should be an assignment before the branch to assign the local value.
                    var beforeLocal = flow.SignificantPreviousStatements(nullCheck);
                    if (beforeLocal.Count() != 1)
                    {
                        this.AddProblem("MustCheckComponentsNull", disposeComponentsCall.SourceContext);
                        return;
                    }

                    var assignmentStatement = beforeLocal.First() as AssignmentStatement;
                    if ((assignmentStatement == null)
                        || !object.ReferenceEquals(assignmentStatement.Target, nullCheck.Condition))
                    {
                        this.AddProblem("MustCheckComponentsNull", disposeComponentsCall.SourceContext);
                        return;
                    }

                    BinaryExpression binaryExpression = null;
                    if (assignmentStatement.Source.NodeType == NodeType.Pop)
                    {
                        // Before the assignment statement there should be two paths. The first should be a TRUE literal
                        // which comes from the if (!disposing) and a this.components == null check. We look at the second
                        // to get the null check.
                        var beforeAssignment = flow.SignificantPreviousStatements(assignmentStatement);
                        if (beforeAssignment.Count() == 2)
                        {
                            // Make sure the first statement is a literal TRUE.
                            var trueLiteral = (beforeAssignment.First() as ExpressionStatement)
                                .IfNotNull(expression => expression.Expression as Literal);
                            if ((trueLiteral == null)
                                || !(trueLiteral.Value is int)
                                || (((int)trueLiteral.Value) != 1))
                            {
                                this.AddProblem("MustDisposeComponents", disposeComponentsCall.SourceContext);
                            }

                            // Get the expression from the null check.
                            (beforeAssignment.Last() as ExpressionStatement)
                                .IfNotNull(expression => expression.Expression as BinaryExpression)
                                .IfNotNull(expression => binaryExpression = expression);
                        }

                        if (binaryExpression == null)
                        {
                            this.AddProblem("MustDisposeComponents", disposeComponentsCall.SourceContext);
                            return;
                        }
                    }
                    else
                    {
                        binaryExpression = assignmentStatement.Source as BinaryExpression;
                        if ((binaryExpression == null)
                            || !IsComponentsBinding(binaryExpression.Operand1 as MemberBinding))
                        {
                            this.AddProblem("MustDisposeComponents", disposeComponentsCall.SourceContext);
                            return;
                        }
                    }

                    var literal = binaryExpression.Operand2 as Literal;
                    if ((literal == null)
                        || (literal.Value != null))
                    {
                        this.AddProblem("MustCheckComponentsNull", disposeComponentsCall.SourceContext);
                        return;
                    }

                    break;

                case NodeType.LogicalNot:
                    var unaryExpression = (UnaryExpression)nullCheck.Condition;
                    if (!IsComponentsBinding(unaryExpression.Operand as MemberBinding))
                    {
                        this.AddProblem("MustCheckComponentsNull", disposeComponentsCall.SourceContext);
                        return;
                    }

                    break;
            }
        }

        /// <summary>
        /// The components field is not created and therefore we need to make sure it is not being used.
        /// </summary>
        /// <param name="flow">The statement flow.</param>
        /// <remarks>
        /// This usually causes an issue where the 'if (disposing &amp;&amp; (this.components != null))' is used so that none of
        /// the code is run since this.components is null.
        /// </remarks>
        private void CheckThatComponentsIsntAccessed(StatementFlow flow)
        {
            Statement closestStatement = null;
            Statement componentsUsage = null;
            new CciVisitor()
                .AddVisitor<MemberBinding>(binding =>
                    {
                        // Keep track of the first statement the components field is used in.
                        if (IsComponentsBinding(binding) && (componentsUsage == null))
                        {
                            componentsUsage = closestStatement;
                        }
                    })
                .AddAllNodeVisitor(node =>
                    {
                        Statement statement = node as Statement;
                        if (statement != null)
                        {
                            closestStatement = statement;
                        }
                    })
                .Visit(flow.Method);

            if (componentsUsage != null)
            {
                this.AddProblem("DontUseComponents", componentsUsage.SourceContext);
            }
        }
        #endregion Private Methods
    }
}